<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="S7SjC" id="S7SjC"><span data-lake-id="u640489c2" id="u640489c2">典型回答</span></h1>
  <p data-lake-id="uc020181f" id="uc020181f"><br></p>
  <p data-lake-id="u29b16c17" id="u29b16c17"><br></p>
  <p data-lake-id="uec04ced5" id="uec04ced5"><span data-lake-id="uc95b4470" id="uc95b4470">本地消息表的分布式事务方案是依赖本地消息表，然后通过定时任务扫表的方式来实现的最终一致性。那么这个方案，整体上来看，在以下几个方面是有问题的：</span></p>
  <p data-lake-id="u53ff3b4c" id="u53ff3b4c"><span data-lake-id="ucc048354" id="ucc048354">​</span><br></p>
  <p data-lake-id="ud228dcc4" id="ud228dcc4" style="text-align: left; padding-left: 2em"><strong><span data-lake-id="u0c16344c" id="u0c16344c">1、消息堆积扫表慢</span></strong></p>
  <p data-lake-id="u622542fd" id="u622542fd" style="text-align: left; padding-left: 2em"><strong><span data-lake-id="u6ce3c9c5" id="u6ce3c9c5">2、集中式扫表会影响正常业务</span></strong></p>
  <p data-lake-id="uff2014bc" id="uff2014bc" style="text-align: left; padding-left: 2em"><strong><span data-lake-id="u8b180ca7" id="u8b180ca7">3、定时扫表存在延迟问题</span></strong></p>
  <p data-lake-id="u9501e933" id="u9501e933" style="text-align: left; padding-left: 2em"><strong><span data-lake-id="uff8a5733" id="uff8a5733">​</span></strong><br></p>
  <p data-lake-id="u1cf79069" id="u1cf79069" style="text-align: left"><span data-lake-id="u836bb5f0" id="u836bb5f0">那么，这几个问题，该如何解决呢？</span></p>
  <p data-lake-id="ua9258953" id="ua9258953" style="text-align: left"><span data-lake-id="u539b18d5" id="u539b18d5">​</span><br></p>
  <h3 data-lake-id="vopry" id="vopry"><span data-lake-id="ub04e90b0" id="ub04e90b0">消息堆积，扫表慢</span></h3>
  <p data-lake-id="uac47c8da" id="uac47c8da"><br></p>
  <p data-lake-id="u6bd882b7" id="u6bd882b7"><span data-lake-id="u5d160de6" id="u5d160de6">随着本地消息表中的数据量越来越大，通过定时任务扫表的方式会越来越慢，那么想要解决这个问题，首先可以考虑加索引。</span></p>
  <p data-lake-id="udbeb3f46" id="udbeb3f46"><span data-lake-id="u730e6f22" id="u730e6f22">​</span><br></p>
  <p data-lake-id="ua77ea282" id="ua77ea282"><span data-lake-id="u03078e10" id="u03078e10">我们可以在state字段上增加一个索引，虽然这个字段的区分度不高，但是一般来说，这张表中，SUCCESS的数据量占90%，而INIT的数据量只占10%，而我们扫表的时候只关心INIT即可，所以增加索引后，扫表的效率是可以大大提升的。</span></p>
  <p data-lake-id="ud310cee8" id="ud310cee8"><span data-lake-id="u5c53f687" id="u5c53f687">​</span><br></p>
  <p data-lake-id="u67ac091e" id="u67ac091e"><br></p>
  <p data-lake-id="u68d5f059" id="u68d5f059"><span data-lake-id="u71a410d9" id="u71a410d9">其次，可以考虑多线程并发扫表，这里可以考虑采用线程池，在任务中开多个线程并发的从数据库中扫描数据进行处理。</span></p>
  <p data-lake-id="u651ba70d" id="u651ba70d"><span data-lake-id="u243b0bf3" id="u243b0bf3">​</span><br></p>
  <p data-lake-id="u6c540b68" id="u6c540b68"><span data-lake-id="u30b2ef7e" id="u30b2ef7e">但是这样做，会带来一个问题，那就是多个线程之间如何做好隔离，如何确保不会出现并发导致同一条记录被多个线程执行多次呢？</span></p>
  <p data-lake-id="u27cbba5f" id="u27cbba5f"><span data-lake-id="u2333d4f8" id="u2333d4f8">​</span><br></p>
  <p data-lake-id="uf3288196" id="uf3288196"><span data-lake-id="ud4ebdca0" id="ud4ebdca0">首先最基本的保障，扫表之后的处理逻辑要做好幂等控制，一旦出现了重复的情况，下游也能因为做了幂等而不会重复处理。</span></p>
  <p data-lake-id="uf109ade7" id="uf109ade7"><span data-lake-id="u8dbb01f8" id="u8dbb01f8">​</span><br></p>
  <p data-lake-id="u6820b0c4" id="u6820b0c4"><span data-lake-id="ud9e567dd" id="ud9e567dd">除此以外，在扫表的时候，可以通过分段的思想进行数据隔离。举个例子：</span></p>
  <p data-lake-id="uba3f703f" id="uba3f703f"><span data-lake-id="u37a9dafa" id="u37a9dafa">​</span><br></p>
  <pre lang="java"><code>
Long minId = messageService.getMinInitId();


for(int i=1;i&lt;= threadPool.size();i++){
    Long maxId = minId + segmentSize()*i;

    List&lt;Message&gt; messages = messageService.scanInitMessages(minId,maxId);

    proccee(messages);
    minId = maxId + 1;
}
</code></pre>
  <p data-lake-id="ueeb3a86c" id="ueeb3a86c"><br></p>
  <p data-lake-id="u3f6ffd0b" id="u3f6ffd0b"><span data-lake-id="u2e308c7d" id="u2e308c7d">像上面的例子中，假设有10个线程，那么第一个线程就扫描ID处于0-1000的数据，第二个线程扫描1001-2000的数据，第三个线程扫描2001-3000的数据。这样以此类推，线程之间通过分段的方式就做好了隔离，可以避免同一个数据被多个线程扫描到。</span></p>
  <p data-lake-id="u6cc004a0" id="u6cc004a0"><span data-lake-id="uca153f4e" id="uca153f4e">​</span><br></p>
  <p data-lake-id="u20dba5db" id="u20dba5db"><span data-lake-id="u7e8117ed" id="u7e8117ed">这个做法，有个小问题，那就是INIT的数据的ID可能不是连续的，那么就需要考虑其他的分段方式，比如在时间表中增加一个业务ID，然后根据这个biz_id做分片也可以。</span></p>
  <p data-lake-id="u62e225b2" id="u62e225b2"><span data-lake-id="uebdab57e" id="uebdab57e">​</span><br></p>
  <p data-lake-id="u039bec64" id="u039bec64"><span data-lake-id="ua11f77c9" id="ua11f77c9">比如：</span></p>
  <pre lang="java"><code>

for(int i=1;i&lt;= threadPool.size();i++){
    List&lt;Message&gt; messages = messageService.scanInitMessages(i);
    proccee(messages);
}
</code></pre>
  <p data-lake-id="u179e308e" id="u179e308e"><br></p>
  <p data-lake-id="u226aed52" id="u226aed52"><span data-lake-id="udca951f6" id="udca951f6">这样在SQL中：</span></p>
  <p data-lake-id="u9ac56b31" id="u9ac56b31"><span data-lake-id="ue7cba3f9" id="ue7cba3f9">​</span><br></p>
  <pre lang="java"><code>
SELECT * FROM RETRY_MESSAGE WHERE 
STATE = "INIT"
AND BIZ_ID LIKE "${frontNumber}%"
</code></pre>
  <p data-lake-id="uab51c6c0" id="uab51c6c0"><span data-lake-id="u7506340e" id="u7506340e">那么，不同的线程执行的SQL就不一样了分别是：</span></p>
  <p data-lake-id="u0c386f3b" id="u0c386f3b"><span data-lake-id="u09db48bf" id="u09db48bf">​</span><br></p>
  <pre lang="java"><code>
SELECT * FROM RETRY_MESSAGE WHERE 
STATE = "INIT"
AND BIZ_ID LIKE "1%"

SELECT * FROM RETRY_MESSAGE WHERE 
STATE = "INIT"
AND BIZ_ID LIKE "2%"

SELECT * FROM RETRY_MESSAGE WHERE 
STATE = "INIT"
AND BIZ_ID LIKE "3%"

SELECT * FROM RETRY_MESSAGE WHERE 
STATE = "INIT"
AND BIZ_ID LIKE "4%"
</code></pre>
  <p data-lake-id="u582db14a" id="u582db14a"><br></p>
  <p data-lake-id="u6a36877f" id="u6a36877f"><span data-lake-id="u75002425" id="u75002425">这样也是可以做分段的。</span></p>
  <p data-lake-id="u71243b90" id="u71243b90"><span data-lake-id="ub1269de9" id="ub1269de9">​</span><br></p>
  <h3 data-lake-id="et3QN" id="et3QN"><strong><span data-lake-id="ucf9d8caa" id="ucf9d8caa">集中式扫表会影响正常业务</span></strong></h3>
  <p data-lake-id="u3539a924" id="u3539a924"><br></p>
  <p data-lake-id="ue9bb2f14" id="ue9bb2f14"><span data-lake-id="u3f4db6f1" id="u3f4db6f1">如果业务量比较大的话，集中式的扫描数据库势必给数据库带来一定的压力，那么就会影响到正常的业务。</span></p>
  <p data-lake-id="uc5bd77d2" id="uc5bd77d2"><span data-lake-id="u40efd551" id="u40efd551">​</span><br></p>
  <blockquote data-lake-id="u6504abae" id="u6504abae">
   <p data-lake-id="uac460f6b" id="uac460f6b"><span data-lake-id="uaf3a7c3d" id="uaf3a7c3d">因为数据量大的话会一直扫表做查询，数据量大的时候查询就会很慢，那么数据库连接数就会被占满。导致应用的正常请求拿不到连接.</span></p>
  </blockquote>
  <p data-lake-id="ufd0e4e57" id="ufd0e4e57"><span data-lake-id="u5159e89b" id="u5159e89b">​</span><br></p>
  <p data-lake-id="u5d24d4e6" id="u5d24d4e6"><span data-lake-id="u641a3711" id="u641a3711">那么想要解决这个问题，首先可以考虑，不扫主库，而是扫描备库。之所以能这么做，是因为这个业务场景一般都是可以接受一定的数据延迟的，那么备库带来延迟就可以忽略，但是备库是没有业务操作的，所以对备库的扫描是不会对业务造成影响的。</span></p>
  <p data-lake-id="ufd93a1bf" id="ufd93a1bf"><span data-lake-id="u09046d7d" id="u09046d7d">​</span><br></p>
  <p data-lake-id="u8f78841b" id="u8f78841b"><span data-lake-id="u1dfd28df" id="u1dfd28df">当然，这里还要考虑一个问题，那就是备库扫描数据之后的执行，执行完该如何同步到主库，这里可以直接修改主库，主备库数据ID一致的，直接去修改主库的就行了。不建议直接在备库上修改。</span></p>
  <p data-lake-id="uf544b90c" id="uf544b90c"><span data-lake-id="u74405a13" id="u74405a13">​</span><br></p>
  <p data-lake-id="u13c23e02" id="u13c23e02"><span data-lake-id="u23df1ef1" id="u23df1ef1">但是不管怎么样，备库还是可以分担扫表的这个大量高峰请求的。</span></p>
  <p data-lake-id="u1da76eff" id="u1da76eff"><span data-lake-id="ua95ef926" id="ua95ef926">​</span><br></p>
  <p data-lake-id="u321433c5" id="u321433c5"><span data-lake-id="ufb55e25d" id="ufb55e25d">除了扫备库，还有一个方案，那就是做分库了。把原来集中在同一个数据库的数据分散到不同的数据库中，这样用集群代替单库来整体对外提供服务，可以大大的提升吞吐量。</span></p>
  <p data-lake-id="u1298e42d" id="u1298e42d"><span data-lake-id="u4aa7951a" id="u4aa7951a">​</span><br></p>
  <p data-lake-id="u2d3b0c21" id="u2d3b0c21"><span data-lake-id="u5b95fa47" id="u5b95fa47">因为多个数据库的话，每个库提供的连接数就会多，并且多个实例的话，CPU、IO、LOAD这些指标也可以互相分担。</span></p>
  <p data-lake-id="u811fe471" id="u811fe471"><span data-lake-id="uf06bca76" id="uf06bca76">​</span><br></p>
  <p data-lake-id="ud7ad5910" id="ud7ad5910"><span data-lake-id="ud1129314" id="ud1129314">​</span><br></p>
  <h3 data-lake-id="iFnMu" id="iFnMu"><strong><span data-lake-id="u65794248" id="u65794248">定时扫表存在延迟问题</span></strong></h3>
  <p data-lake-id="u62b59c07" id="u62b59c07"><br></p>
  <p data-lake-id="u8fc82585" id="u8fc82585"><span data-lake-id="u605944cc" id="u605944cc">定时任务都是集中式的定时执行的，那么就会存在延迟的问题。随着数据库越来越大，延时会越来越长。</span></p>
  <p data-lake-id="u6cb6fcd0" id="u6cb6fcd0"><span data-lake-id="u09e64a4a" id="u09e64a4a">​</span><br></p>
  <p data-lake-id="u81f3a8d9" id="u81f3a8d9"><span data-lake-id="ueb3e12fc" id="ueb3e12fc">想要降低延迟，那就要抛弃定时任务的方案，可以考虑延迟消息，基于延迟消息来做定时执行。</span></p>
  <p data-lake-id="uc0322661" id="uc0322661"><span data-lake-id="ud7c2ac0b" id="ud7c2ac0b">​</span><br></p>
  <p data-lake-id="u61223f75" id="u61223f75"><span data-lake-id="ud1ff98c4" id="ud1ff98c4">用了延迟消息之后，还可以缓解数据库的压力。也能比定时扫表的性能要好，实时性也更高。</span></p>
  <p data-lake-id="ubd124613" id="ubd124613"><span data-lake-id="ufd74ee4a" id="ufd74ee4a">​</span><br></p>
  <p data-lake-id="uf2a56bd1" id="uf2a56bd1"><br></p>
  <p data-lake-id="u9c0c37f3" id="u9c0c37f3"><span data-lake-id="ua001596c" id="ua001596c">当然，引入另外一个中间件也需要考虑成本的。</span></p>
  <p data-lake-id="ud913df39" id="ud913df39"><span data-lake-id="ud8e4b8c2" id="ud8e4b8c2">​</span><br></p>
  <h3 data-lake-id="k7Aal" id="k7Aal"><span data-lake-id="ubf2e7931" id="ubf2e7931">同步转异步</span></h3>
  <p data-lake-id="u27909946" id="u27909946"><br></p>
  <p data-lake-id="ue3c167c6" id="ue3c167c6"><span data-lake-id="u08c1c80c" id="u08c1c80c">再提一个方案，那就是同步转异步。什么叫同步转异步呢，那就是同步先干一把，失败了的话，再异步执行。如：</span></p>
  <p data-lake-id="u7d05ccc0" id="u7d05ccc0"><span data-lake-id="u695dc63e" id="u695dc63e">​</span><br></p>
  <pre lang="java"><code>
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
    .setNameFormat("hollis-pool-%d").build();

private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue&lt;Runnable&gt;(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

@Transactional(rollbackFor = Exception.class)
public void pay(PayRequest payRequest){

	//在同一个事务中做本地业务操作和记录消息
	payService.doPay(payRequest);
    retryMessageService.init(payRequest);

	//同步执行一次外部调用
    try{
        Result result = outerService.doSth(payRequest);
        if(result.isSuccess()){
            retryMessageService.success(payRequest);
        }
    }catch(Exception e){
        // 捕获异常，失败依赖异步重试
    }
	

}

</code></pre>
  <p data-lake-id="uafb76f1e" id="uafb76f1e"><br></p>
  <p data-lake-id="uc3d664ab" id="uc3d664ab"><span data-lake-id="ue9f3b9c8" id="ue9f3b9c8">如上，在同步接口中，先尝试着执行一次要可能会失败的任务，如果成功了，那就把事件推进到成功，。如果失败了也无所谓，因为会有异步定时任务捞起来重试。</span></p>
  <p data-lake-id="ufd2724b9" id="ufd2724b9"><span data-lake-id="u232a24b4" id="u232a24b4">​</span><br></p>
  <p data-lake-id="uf97fa4da" id="uf97fa4da"><span data-lake-id="u22e19751" id="u22e19751">​</span><br></p>
 </body>
</html>